import datetime
import functools

import tushare as ts
from django.db import models

from processor.utils import *
from config import *

''' 初始化 Tushare接口
'''
ts.set_token(TUSHARE_TOKEN)
TS_PRO = ts.pro_api()


class StockBasic(models.Model):
    '''
    模型: 股票基础信息
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, unique=True, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    list_status = models.CharField(max_length=16, verbose_name="[LIST_STATUS] 上市状态")
    list_date = models.DateField(verbose_name="[LIST_DATE] 上市日期")

    last_update = models.DateField(verbose_name="[LAST_UPDATE] 最后更新")
    suspension = models.JSONField(verbose_name="[TUSHARE_SOURCE] 停牌日集")
    tushare_source = models.JSONField(verbose_name="[TUSHARE_SOURCE] 日级Tushare源", default=dict)
    tushare_index = models.JSONField(verbose_name="[TUSHARE_INDEX] 日级Tushare索引", default=list)

    tushare_weekly_source = models.JSONField(verbose_name="[WEEKLY_TUSHARE_SOURCE] 周级Tushare源", default=dict)
    tushare_weekly_index = models.JSONField(verbose_name="[WEEKLY_TUSHARE_INDEX] 周级Tushare索引", default=list)

    wind_source = models.JSONField(verbose_name="[WIND_SOURCE] 日级Wind源", default=dict)
    wind_index = models.JSONField(verbose_name="[WIND_INDEX] 日级Wind索引", default=list)

    wind_weekly_source = models.JSONField(verbose_name="[WEEKLY_WIND_SOURCE] 周级Wind源", default=dict)
    wind_weekly_index = models.JSONField(verbose_name="[WEEKLY_WIND_INDEX] 周级Wind索引", default=list)

    @classmethod
    def update_by_code(cls, scode):
        basic_info = TS_PRO.stock_basic(
            ts_code = scode,
            fields = 'ts_code, name, industry, list_date, list_status',
        )
        assert not basic_info.empty, "Could not find the stock code: " + str(scode)
        basic_info = basic_info.iloc[0]
        suspend_date = TS_PRO.suspend_d(
            ts_code = scode, 
            suspend_type = 'S'
        ).dropna(
            subset=['trade_date']
        )['trade_date'].map(
            lambda x: x[:4] + '-' + x[4:6] + '-' + x[6:]
        ).to_list()
        _, created = StockBasic.objects.update_or_create(
            scode = scode,
            defaults = {
                'sname': basic_info['name'],
                'industry': basic_info['industry'] if basic_info['industry'] is not None else "",
                'last_update': datetime.datetime.strptime(DEFAULT_START_DATE, '%Y-%m-%d'),
                'list_status': basic_info['list_status'],
                'list_date': datetime.datetime.strptime(basic_info['list_date'], '%Y%m%d'),
                'suspension': suspend_date,
                'tushare_source': {},
                'tushare_index': [],
                'wind_source': {},
                'wind_index': [],
                'wind_weekly_source': {},
                'wind_weekly_index': [],
            }
        )
        return created

    @classmethod
    def get_by_code(cls, scode):
        if not cls.objects.filter(scode = scode).exists():
            cls.update_by_code(scode)
        return cls.objects.get(scode = scode)


class StockRSI(models.Model):
    '''
    模型: 股票RSI数据
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, unique=True, verbose_name="[SCODE] 股票代码")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, verbose_name="[STOCK_BASIC] 股票基础信息")

    rsi = models.JSONField(verbose_name="[RSI] 日级RSI指标")
    rsi_index = models.JSONField(verbose_name="[RSI_INDEX] 日级RSI索引")

    wrsi = models.JSONField(verbose_name="[WRSI] 周级RSI指标")
    wrsi_index = models.JSONField(verbose_name="[WRSI_INDEX] 周级RSI索引")

    @classmethod
    def get_by_code(cls, scode):
        if not cls.objects.filter(scode = scode).exists():
            stock_basic = StockBasic.get_by_code(scode)
            if stock_basic is None: return None
            return cls.objects.create(
                scode = scode,
                stock_basic = stock_basic,
                rsi = {},
                rsi_index = [],
                wrsi = {},
                wrsi_index = [],
            )
        return cls.objects.get(scode = scode)


class RSIExtremum(models.Model):
    '''
    模型: 股票RSI极值
    '''
    PERIODS = (
        (1, 'Daily'),
        (2, 'Weekly'),
    )
    INDEX_FIELDS = ['rsi_maxd', 'rsi_mind', 'rsi_maxdx', 'rsi_mindx']

    period = models.IntegerField(choices=PERIODS, default=1, verbose_name="[PERIOD] 筛选周期")

    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    
    stock_rsi = models.ForeignKey(StockRSI, editable=False, on_delete=models.CASCADE, verbose_name="[STOCK_RSI] 股票RSI数据")

    lastest_rsi = models.FloatField(default=0, verbose_name="[LASTEST_RSI] 最新RSI")

    rsi_maxd = models.FloatField(default=0, verbose_name="[RSI_MAXD] 历史最大值")
    rsi_mind = models.FloatField(default=100, verbose_name="[RSI_MIND] 历史最小值")
    rsi_maxdx = models.FloatField(default=0, verbose_name="[RSI_MAXDX] 范围最大值")
    rsi_mindx = models.FloatField(default=100, verbose_name="[RSI_MINDX] 范围最小值")

    rsi_maxd_date = models.DateField(default=datetime.date(1900, 1, 1), verbose_name="[RSI_MAXD_DATE] 历史最大值日期")
    rsi_mind_date = models.DateField(default=datetime.date(1900, 1, 1), verbose_name="[RSI_MIND_DATE] 历史最小值日期")
    rsi_maxdx_date = models.DateField(default=datetime.date(1900, 1, 1), verbose_name="[RSI_MAXDX_DATE] 范围最大值日期")
    rsi_mindx_date = models.DateField(default=datetime.date(1900, 1, 1), verbose_name="[RSI_MINDX_DATE] 范围最小值日期")
    
    # 此处用 4 位二进制表示各极值的更新状态， 即 0010 代表 rsi_maxdx 字段更新，其余字段未更新
    updated_field = models.IntegerField(db_index=True, default=0, verbose_name="[UPDATE_FIELD] 更新字段")
    last_update = models.DateField(auto_now_add=True, verbose_name="[LAST_UPDATE] 最后更新")
    
    class Meta:
        index_together = [
            ['last_update', 'stock_rsi']
        ]
        unique_together = [
            ['period', 'scode'],
        ]

    def set_extremum(self, field, value, date):
        '''
            方法： 设定极值
            参数：
                field:      字段
                    rsi_maxd
                    rsi_mind
                    rsi_maxdx
                    rsi_mindx
                value:      RSI 值
                date:       日期
        '''
        fields = {'rsi_maxd': 8, 'rsi_mind': 4, 'rsi_maxdx': 2, 'rsi_mindx': 1}
        assert field in fields, "Invalid parameter: rsi field"

        assert value is not None and isinstance(value, (int, float)), \
            "Invalid parameter: rsi value"
        
        setattr(self, field, value)
        setattr(self, f'{field}_date', date)
        setattr(self, 'updated_field', getattr(self, 'updated_field') | fields[field])
        

class RSIStructure(models.Model):
    '''
    模型: 每日数据分析
    '''
    sdate = models.DateField(unique=True, editable=False, verbose_name="[SDATE] 分析日期")
    percentage = models.JSONField(verbose_name="[PERCENTAGE] 各层级占比", default=list)

    rb1 = models.JSONField(verbose_name="[RB1] RB1", default=list)
    rb2 = models.JSONField(verbose_name="[RB2] RB2", default=list)
    rb3 = models.JSONField(verbose_name="[RB3] RB3", default=list)
    rb4 = models.JSONField(verbose_name="[RB4] RB4", default=list)
    rb5 = models.JSONField(verbose_name="[RB5] RB5", default=list)
    rb6 = models.JSONField(verbose_name="[RB6] RB6", default=list)

    rb1_cnt = models.IntegerField(verbose_name="[RB1_CNT] RB1 个数", default=0)
    rb2_cnt = models.IntegerField(verbose_name="[RB2_CNT] RB2 个数", default=0)
    rb3_cnt = models.IntegerField(verbose_name="[RB3_CNT] RB3 个数", default=0)
    rb4_cnt = models.IntegerField(verbose_name="[RB4_CNT] RB4 个数", default=0)
    rb5_cnt = models.IntegerField(verbose_name="[RB5_CNT] RB5 个数", default=0)
    rb6_cnt = models.IntegerField(verbose_name="[RB6_CNT] RB6 个数", default=0)


# class RSIInflection(models.Model):
#     '''
#     模型: RSI 波谷拐点
#     '''
#     scode = models.CharField(max_length=16, db_index=True, editable=False, unique=True, verbose_name="[SCODE] 股票代码")
#     stock_rsi = models.ForeignKey(StockRSI, editable=False, on_delete=models.CASCADE, verbose_name="[STOCK_RSI] 股票RSI数据")
#     inflect = models.JSONField(verbose_name="[INFLECT] 日级拐点")
#     winflect = models.JSONField(verbose_name="[WINFLECT] 周级拐点")
# 
#     @classmethod
#     def get_by_code(cls, scode):
#         if not cls.objects.filter(scode = scode).exists():
#             stock_rsi = StockRSI.get_by_code(scode)
#             if stock_rsi is None: return None
#             return cls.objects.create(
#                 scode = scode,
#                 stock_rsi = stock_rsi,
#                 inflect = {},
#                 winflect = {},
#             )
#         return cls.objects.get(scode = scode)


class RSITrend(models.Model):
    '''
    模型: 趋势判断筛选
    '''
    PERIODS = (
        (1, 'Daily'),
        (2, 'Weekly'),
    )
    DAILY = 1
    WEEKLY = 2
    fdate = models.DateField(editable=False, verbose_name="[FDATE] 筛选日期")
    period = models.IntegerField(choices=PERIODS, editable=False, verbose_name="[PERIOD] 筛选周期")

    bg_a = models.JSONField(default=list, verbose_name="[BG_A] bg-A数据")
    bg_b = models.JSONField(default=list, verbose_name="[BG_B] bg-B数据")
    bg_c = models.JSONField(default=list, verbose_name="[BG_C] bg-C数据")
    bg_d = models.JSONField(default=list, verbose_name="[BG_D] bg-D数据")
    bg_e = models.JSONField(default=list, verbose_name="[BG_E] bg-E数据")
    bg_f = models.JSONField(default=list, verbose_name="[BG_F] bg-F数据")

    class Meta:
        unique_together = [
            ('fdate', 'period'),
        ]


class MACPool(models.Model):
    '''
    模型: 均线交叉池
    '''
    POOLS = (
        (1, 'Pool1'),
        (2, 'Pool2'),
        (3, 'Pool3'),
    )
    SOURCES = (
        (1, "Tushare"),
        (2, "Wind"),
    )

    pid = models.IntegerField(choices=POOLS, db_index=True, editable=False, verbose_name="[PID] 观察池{1,2,3}")
    source = models.IntegerField(choices=SOURCES, db_index=True, editable=False, verbose_name="[SOURCE] 源")

    fdate = models.DateField(db_index=True, editable=False, verbose_name="[FDATE] 筛选日期")
    field = models.JSONField(verbose_name="[FIELD] 池数据")


    @classmethod
    def inject(cls, pool = 1, source = 1, date = '', record = {}):
        '''
        方法: 向池中注入记录
            参数: 
                pool:       观察池 ID
                source:     源 ID
                date:       筛选时间
                scode:      股票代码
                time:       池内时间
        '''
        assert date and record, "观察池插入失败，股票代码及筛选时间不能为空！"
        pool, _ = MACPool.objects.get_or_create(
            pid = pool,
            source = source,
            fdate = date,
            defaults = {
                'field': {},
            }
        )
        pool.field.update(record)
        pool.save()
        


class Logger(models.Model):
    '''
    模型: 日志记录
    '''
    LOG_LEVEL = (
        (0, "DEBUG"),       # 调试信息
        (1, "INFO"),        # 通知信息
        (2, "WARNING"),     # 警告信息
        (3, "ERROR"),       # 错误信息
        (4, "CRITICAL"),    # 危机信息
    )
    MODULES = (
        ('general', "通用"),
        ('crawler', "数据中心"),
        ('function', "计算中心"),
        ('scheduler', "调度器"),
        ('monitor', "前台"),
        ('export', "数据导出"),
    )
    timestamp =  models.DateTimeField(editable=False, auto_now_add=True, db_index=True, verbose_name="[TIMESTAMP] 记录时间")
    level = models.IntegerField(choices=LOG_LEVEL, db_index=True, editable=False, verbose_name="[LEVEL] 日志等级")
    module = models.CharField(choices=MODULES, max_length=16, default='general', editable=False, verbose_name="[MODULE] 模块")

    message = models.TextField(verbose_name="[MESSAGE] 记录信息")

    @staticmethod
    def logging(code, msg, module='general', callback=print):
        '''
        接口: 日志记录
            传入参数:
                code:       日志等级
                msg:        记录信息
                callback:   回调函数
        '''
        try:
            assert code in range(5), "Invalid level parameter received. "
            Logger(level=code, message=msg, module=module).save()
            callback and callback(f'{datetime.datetime.now()} [{Logger.LOG_LEVEL[code][1]}] {msg}')
        except Exception as e:
            callback and callback(f'Create Log Record Error: {e}')

    debug = functools.partialmethod(logging, 0)
    info = functools.partialmethod(logging, 1)
    warning = functools.partialmethod(logging, 2)
    error = functools.partialmethod(logging, 3)
    critical = functools.partialmethod(logging, 4)


class StockTop10Holder(models.Model):
    '''
    模型: 前十大股东
    '''
    scode = models.CharField(max_length=16, db_index=True, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    stock_basic = models.ForeignKey(StockBasic, on_delete=models.CASCADE, related_name='top10holder', verbose_name="[STOCK_BASIC] 股票基础信息")

    quarter = models.CharField(max_length=6, db_index=True, verbose_name="[QUARTER] 统计季度")
    report_date = models.DateField(verbose_name="[REPORT_DATE] 报告日期")

    hold_info = models.JSONField(verbose_name="[HOLD] 当前持股信息")
    holders = models.TextField(verbose_name="[HOLDERS] 当前持股人")

    # prop_change = models.FloatField(default=0, verbose_name="[PROP_CHANGE] 占比差值")

    new_holder_count = models.IntegerField(default=0, verbose_name="[NEW_HOLDER_COUNT] 新晋股东数量")
    new_holder_prop = models.FloatField(default=0, verbose_name="[NEW_HOLDER_PROP] 新晋持股占比")

    # new_fund_count = models.IntegerField(default=0, verbose_name="[NEW_FUND_COUNT] 新晋基金股东数量")
    # new_fund_prop = models.FloatField(default=0, verbose_name="[NEW_FUND_PROP] 新晋基金持股占比")

    add_holder_count = models.IntegerField(default=0, verbose_name="[ADD_HOLDER_COUNT] 增加股东数量")
    add_holder_prop = models.FloatField(default=0, verbose_name="[ADD_HOLDER_PROP] 增加持股占比")

    reduce_holder_count = models.IntegerField(default=0, verbose_name="[REDUCE_HOLDER_COUNT] 减少股东数量")
    reduce_holder_prop = models.FloatField(default=0, verbose_name="[REDUCE_HOLDER_PROP] 减少持股占比")

    class Meta:
        unique_together = ['quarter', 'scode']


class Shareholding(models.Model):
    '''
    模型： 持股
    '''
    CHANGE_TYPES = [
        (-1, "减少"),
        (0, "不变"),
        (1, "增加"),
        (2, "新增"),
    ]
    scode = models.CharField(max_length=16, db_index=True, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    stock_basic = models.ForeignKey(StockBasic, on_delete=models.CASCADE, related_name='shareholding', verbose_name="[STOCK_BASIC] 股票基础信息")
    concepts = models.CharField(max_length=512, verbose_name="[CONCEPTS] 所属概念")

    quarter = models.CharField(max_length=6, db_index=True, verbose_name="[QUARTER] 统计季度")

    holder = models.CharField(max_length=128, verbose_name="[HOLDER] 持股人")
    is_personal = models.BooleanField(default=False, verbose_name="[IS_PERSONAL] 是否是散户")
    is_legend = models.BooleanField(default=False, verbose_name="[IS_LEGEND] 是否是牛散")

    change_type = models.IntegerField(default=0, choices=CHANGE_TYPES, verbose_name="[CHANGE_TYPE] 变化类型")
    prop_change = models.FloatField(default=0, verbose_name="[PROP_CHANGE] 比例变化")

    hold_prop = models.FloatField(default=0, verbose_name="[HOLD_PROP] 持股比例")
    hold_amount = models.BigIntegerField(default=0, verbose_name="[HOLD_AMOUNT] 持股市值")

    prev_hold_prop = models.FloatField(default=0, verbose_name="[HOLD_PROP] 上期持股比例")
    prev_hold_amount = models.BigIntegerField(default=0, verbose_name="[HOLD_AMOUNT] 上期持股市值")

    objects = TruncateManager()

    class Meta:
        unique_together = [
            ('quarter', 'scode', 'holder'),
        ]
        index_together = [
            ('quarter', 'change_type'),
            ('quarter', 'is_personal'),
            ('quarter', 'holder'),
            ('quarter', 'is_legend', 'holder'),
            ('quarter', 'scode', 'is_legend'),
        ]


class KeywordCombination(models.Model):
    '''
    模型: 关键词组合
    '''
    cname = models.CharField(max_length=32, unique=True, verbose_name="[CNAME] 组合名称")
    keyword_list = models.JSONField(default=list, verbose_name="[keyword_list] 关键词列表")
    exclude_keyword_list = models.JSONField(default=list, verbose_name="[keyword_list] 关键词列表")


class StockHolderNumber(models.Model):
    '''
    模型: 股东户数
    '''
    scode = models.CharField(max_length=16, editable=False, unique=True, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")

    stock_basic = models.OneToOneField(StockBasic, editable=False, on_delete=models.CASCADE, related_name='holdernumber', verbose_name="[STOCK_BASIC] 股票基础信息")

    updated_date = models.DateField(auto_now=True, verbose_name="[UPDATE_DATE] 更新日期")

    t_0_d = models.DateField(verbose_name="[T0D] T日期")
    t_q_d = models.DateField(null=True, verbose_name="[TQD] T-Q日期")
    t_30_d = models.DateField(null=True, verbose_name="[T30D] T-30日期")
    t_60_d = models.DateField(null=True, verbose_name="[T60D] T-60日期")
    t_90_d = models.DateField(null=True, verbose_name="[T90D] T-90日期")
    t_180_d = models.DateField(null=True, verbose_name="[T180D] T-180日期")
    t_360_d = models.DateField(null=True, verbose_name="[T360D] T-360日期")

    t_0_c = models.IntegerField(verbose_name="[T0D] T日户数")
    t_q_c = models.IntegerField(null=True, verbose_name="[TQD] T-Q日户数")
    t_30_c = models.IntegerField(null=True, verbose_name="[T30D] T-30日户数")
    t_60_c = models.IntegerField(null=True, verbose_name="[T60D] T-60日户数")
    t_90_c = models.IntegerField(null=True, verbose_name="[T90D] T-90日户数")
    t_180_c = models.IntegerField(null=True, verbose_name="[T180D] T-180日户数")
    t_360_c = models.IntegerField(null=True, verbose_name="[T360D] T-360日户数")

    t_q_r = models.FloatField(null=True, verbose_name="[TQD] T-Q变化比例")
    t_30_r = models.FloatField(null=True, verbose_name="[T30D] T-30变化比例")
    t_60_r = models.FloatField(null=True, verbose_name="[T60D] T-60变化比例")
    t_90_r = models.FloatField(null=True, verbose_name="[T90D] T-90变化比例")
    t_180_r = models.FloatField(null=True, verbose_name="[T180D] T-180变化比例")
    t_360_r = models.FloatField(null=True, verbose_name="[T360D] T-360变化比例")


class StockAchievement(models.Model):
    '''
    模型: 业绩筛选
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, related_name='achievement', verbose_name="[STOCK_BASIC] 股票基础信息")
    quarter = models.CharField(max_length=8, verbose_name="[QUARTER] 统计季度")
    report_date = models.DateField(verbose_name="[REPORT_DATE] 报告日期")

    roe = models.FloatField(default=0, verbose_name="[ROE] ROE")
    revenue = models.FloatField(default=0, verbose_name="[REVENUE] 营业收入")
    single_revenue = models.FloatField(default=0, verbose_name="[SINGLE_REVENUE] 单季度营业收入")
    total_cogs = models.FloatField(default=0, verbose_name="[TOTAL_COGS] 营业成本")
    profit_dedt = models.FloatField(default=0, verbose_name="[PROFID_DEDT] 扣非后净利润")
    single_profit_dedt = models.FloatField(default=0, verbose_name="[SINGLE_PROFID_DEDT] 扣非后净利润")
    profit_to_gr = models.FloatField(default=0, verbose_name="[PROFIT_TO_GR] 净利率")
    assets_turn = models.FloatField(default=0, verbose_name="[ASSETS_TURN] 总资产周转率")
    saleexp_to_gr = models.FloatField(default=0, verbose_name="[SALEEXP_TO_GR] 销售费用率")
    per_capita_hold = models.FloatField(default=0, verbose_name="[PER_CAPITA_HOLD] 人均持股")

    class Meta:
        index_together = [
            ['quarter', 'scode']
        ]


class StockForecast(models.Model):
    '''
    模型: 业绩预告
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, related_name='forecast', verbose_name="[STOCK_BASIC] 股票基础信息")
    quarter = models.CharField(max_length=8, verbose_name="[QUARTER] 统计季度")
    report_date = models.DateField(verbose_name="[REPORT_DATE] 报告日期")

    type = models.CharField(max_length=4, verbose_name="[TYPE] 业绩预告类型")
    p_change_min = models.FloatField(default=0, verbose_name="[P_CHANGE_MIN] 预告净利润变动幅度下限")
    p_change_max = models.FloatField(default=0, verbose_name="[P_CHANGE_MAX] 预告净利润变动幅度上限")
    net_profit_min = models.FloatField(default=0, verbose_name="[NET_PROFIT_MIN] 预告净利润下限")
    net_profit_max = models.FloatField(default=0, verbose_name="[NET_PROFIT_MAX] 预告净利润上限")
    last_period_income = models.FloatField(default=0, verbose_name="[LAST_PERIOD_INCOME] 上年同期净利润")
    last_year_income = models.FloatField(default=0, verbose_name="[LAST_YEAR_INCOME] 上年度净利润")

    intraday_close = models.FloatField(default=0, verbose_name="[INTRADAY_CLOSE] 公告日收盘价")
    intraday_pe_ttm = models.FloatField(default=0, verbose_name="[INTRADAY_PE_TTM] 公告日PE(TTM)")
    intraday_total_share = models.FloatField(default=0, verbose_name="[INTRADAY_TOTAL_SHARE] 公告日总股本")
    intraday_total_mv = models.FloatField(default=0, verbose_name="[INTRADAY_TOTAL_MV] 公告日市值")

    annual_profit_min = models.FloatField(default=0, verbose_name="[ANNUAL_PROFIT_MIN] 预告年度净利润下限")
    annual_profit_max = models.FloatField(default=0, verbose_name="[ANNUAL_PROFIT_MAX] 预告年度净利润上限")
    annual_pe = models.FloatField(default=0, verbose_name="[ANNUAL_PE] 预告年度PE")
    peg = models.FloatField(default=0, verbose_name="[PEG] 预告年度PEG")


class FundPortfolio(models.Model):
    '''
    模型: 公募基金持股
    '''
    fcode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[FCODE] 基金代码")
    fname = models.CharField(max_length=64, db_index=True, verbose_name="[FNAME] 基金名称")
    fund_type = models.CharField(max_length=8, db_index=True, verbose_name="[FUND_TYPE] 投资类型")
    fd_share = models.FloatField(default=0, verbose_name="[FD_SHARE] 基金份额")

    quarter = models.CharField(max_length=8, verbose_name="[QUARTER] 统计季度")
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, related_name='funds', verbose_name="[STOCK_BASIC] 股票基础信息")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    mkv = models.FloatField(default=0, verbose_name="[MKV] 持有股票市值")
    amount = models.FloatField(default=0, verbose_name="[AMOUNT] 持有股票数量")
    stk_mkv_ratio = models.FloatField(default=0, verbose_name="[STK_MKV_RATIO] 占股票市值比")
    stk_float_ratio = models.FloatField(default=0, verbose_name="[STK_FLOAT_RATIO] 占流通股本比例")

    total_share = models.FloatField(default=0, verbose_name="[TOTAL_SHARE] 总股本")
    float_share = models.FloatField(default=0, verbose_name="[FLOAT_SHARE] 流通股本")


class ConsensusForecasts(models.Model):
    '''
    模型: 一致预期
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, related_name='consensus', verbose_name="[STOCK_BASIC] 股票基础信息")

    quarter = models.CharField(max_length=8, verbose_name="[QUARTER] 当前季度")
    compute_date = models.DateField(auto_now_add=True, verbose_name="[COMPUTE_DATE] 计算日期")

    net_profit_fy0 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY0] FY0净利润")
    net_profit_fy1 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY1] FY1净利润")
    net_profit_fy2 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY2] FY2净利润")

    forecast_list = models.JSONField(verbose_name="[FORECAST_LIST] 预测清单")


class StockConcept(models.Model):
    '''
    模型: 概念分类
    '''
    ccode = models.CharField(max_length=16, db_index=True, unique=True, verbose_name="[CCODE] 概念代码")
    cname = models.CharField(max_length=32, db_index=True, verbose_name="[CNAME] 概念名称")

    stock_basic_set = models.ManyToManyField(StockBasic, related_name='concepts', verbose_name="[STOCK_BASIC_SET] 所属股票集")
    stock_basic_count = models.IntegerField(default=0, verbose_name="[STOCK_BASIC_COUNT] 所属股票数")

    last_update = models.DateField(auto_now=True, verbose_name="[LAST_UPDATE] 最后更新")

    class StockConceptManager(models.Manager):
        _cached_last_update = None

        @property
        def last_update_date(self):
            if self._cached_last_update is None:
                self._cached_last_update = self.aggregate(
                    last_update=models.Max('last_update')
                )['last_update']
            
            return self._cached_last_update

        @property
        def latest_relation(self):
            # 获取最新关系
            today = datetime.date.today()
            if self.last_update_date == today:
                return self
            
            # 拉取概念清单
            concepts = TS_PRO.ths_index()

            # 筛选 A 股概念 排除以 86 开头的概念
            concepts = concepts[concepts.exchange=='A']
            concepts = concepts[~concepts.ts_code.str.startswith('86')]

            # 历史概念
            history_concepts = list(self.values_list('ccode', flat=True))
            # 更新概念数量
            ConceptHistory.objects.update_or_create(
                source=1,
                update_date=today,
                defaults=dict(
                    concept_count=len(concepts),
                    add_count=len(concepts[~concepts.ts_code.isin(history_concepts)]),
                    reduce_count=len(history_concepts) - len(concepts[concepts.ts_code.isin(history_concepts)]),
                )
            )

            # 预抓取 StockBasic 进行绑定
            basic_dict = {basic.scode: basic for basic in StockBasic.objects.only('scode', 'id')}

            # 获取 概念 成分股 关系
            stocks = tushare_batch_fetch(
                TS_PRO.ths_member, batch_size=5000,
            ).rename(
                {'con_code': 'code', 'con_name': 'name'}, axis=1
            )

            # 删除历史数据
            # StockConcept.objects.all().delete()
            for _, concept in concepts.iterrows():
                # 成分股
                components = stocks[stocks.ts_code == concept.ts_code]

                # 更新此概念
                stock_concept, _ = StockConcept.objects.update_or_create(
                    ccode=concept.ts_code, 
                    defaults=dict(
                        cname=concept['name'],
                        stock_basic_count=len(components),
                    )
                )

                # 更新变动历史
                StockConceptHistory.objects.record(
                    source=1,
                    ccode=concept.ts_code,
                    cname=concept['name'],
                    stocks_list=components[['code', 'name']].to_dict(orient='records'),
                )

                # 绑定关系
                stock_concept.stock_basic_set.set([
                    basic_dict[scode] 
                    for scode in components.code.to_list() 
                    if scode in basic_dict
                ])

            Logger.info(msg=f"Tushare 概念分类已更新至： {today}。", module='crawler')
            self._cached_last_update = today

            return self
        
        def relation_dataframe(self):
            return pd.DataFrame(
                self.latest_relation.values(
                    'ccode', 'cname', cid=models.F('id'),
                ).annotate(
                    sid=models.F('stock_basic_set__id'),
                    scode=models.F('stock_basic_set__scode'),
                    sname=models.F('stock_basic_set__sname'),
                )
            )

    objects = StockConceptManager()


class StockConceptHistory(models.Model):
    '''
    模型：概念股历史
    '''
    SOURCES = (
        (1, "Tushare"),
        (2, "Wind"),
    )

    source = models.IntegerField(choices=SOURCES, db_index=True, editable=False, verbose_name="[SOURCE] 源")
    update_date = models.DateField(verbose_name="[UPDATE_DATE] 交易日期")

    ccode = models.CharField(max_length=16, db_index=True, verbose_name="[CCODE] 概念代码")
    cname = models.CharField(max_length=32, db_index=True, verbose_name="[CNAME] 概念名称")
    
    stock_count = models.IntegerField(default=0, verbose_name="[STOCK_COUNT] 成分股个数")
    stocks_list = models.JSONField(default=list, verbose_name="[STOCKS_LIST] 成分股清单")

    previous_stock_count = models.IntegerField(default=0, verbose_name="[STOCK_COUNT] 上期成分股个数")
    previous_stocks_list = models.JSONField(default=list, verbose_name="[STOCKS_LIST] 上期成分股清单")

    class Meta:
        unique_together = [
            ('source', 'update_date', 'ccode'),
        ]
        index_together = [
            ('source', 'ccode'),
            ('update_date', 'ccode'),
        ]

    class StockConceptHistoryManager(models.Manager):
        
        def record(self, source, ccode, cname, stocks_list, update_date=None):
            '''
            记录概念股变动历史
            '''
            if update_date is None:
                update_date = datetime.date.today()

            # 上次更新记录
            last_record = self.filter(
                source=source,
                ccode=ccode,
            ).exclude(
                update_date=update_date
            ).last()

            # 上期股票数量、清单
            previous_stock_count = 0
            previous_stocks_list = []

            if last_record is not None:
                previous_stock_count = last_record.stock_count
                previous_stocks_list = last_record.stocks_list

            # 写入数据
            StockConceptHistory.objects.update_or_create(
                source=source,
                update_date=update_date,
                ccode=ccode,
                defaults=dict(
                    cname=cname,
                    stock_count=len(stocks_list),
                    stocks_list=stocks_list,
                    previous_stock_count=previous_stock_count,
                    previous_stocks_list=previous_stocks_list,
                )
            )

    objects = StockConceptHistoryManager()


class ConceptHistory(models.Model):
    '''
    模型：概念历史
    '''
    SOURCES = (
        (1, "Tushare"),
        (2, "Wind"),
    )

    source = models.IntegerField(choices=SOURCES, db_index=True, editable=False, verbose_name="[SOURCE] 源")
    update_date = models.DateField(verbose_name="[UPDATE_DATE] 交易日期")

    concept_count = models.IntegerField(default=0, verbose_name="[CONCEPT_COUNT] 概念个数")
    add_count = models.IntegerField(default=0, verbose_name="[ADD_COUNT] 增加个数")
    reduce_count = models.IntegerField(default=0, verbose_name="[REDUCE_COUNT] 减少个数")

    class Meta:
        unique_together = [
            ('source', 'update_date'),
        ]


class ConceptQuarter(models.Model):
    '''
    模型: 概念季度统计
    '''
    stock_concept = models.ForeignKey(StockConcept, on_delete=models.CASCADE, verbose_name="[STOCK_CONCEPT] 股票基础信息")
    quarter = models.CharField(max_length=8, verbose_name="[QUARTER] 当前季度")

    revenue = models.FloatField(default=0, verbose_name="[REVENUE] 营业收入")
    revenue_top3 = models.FloatField(default=0, verbose_name="[REVENUE_TOP3] 营业收入前三占比")
    revenue_top5 = models.FloatField(default=0, verbose_name="[REVENUE_TOP5] 营业收入前五占比")
    revenue_yoy = models.FloatField(default=0, verbose_name="[REVENUE_YOY] 营业收入同比增长")
    # revenue_qoq = models.FloatField(default=0, verbose_name="[REVENUE_QOQ] 营业收入环比增长")

    profit_dedt = models.FloatField(default=0, verbose_name="[PROFID_DEDT] 扣非后净利润")
    # profit_dedt_top3 = models.FloatField(default=0, verbose_name="[PROFID_DEDT_TOP3] 扣非后净利润前三占比")
    # profit_dedt_top5 = models.FloatField(default=0, verbose_name="[PROFID_DEDT_TOP5] 扣非后净利润前五占比")
    profit_dedt_yoy = models.FloatField(default=0, verbose_name="[PROFID_DEDT_YOY] 扣非后净利润同比增长")
    # profit_dedt_qoq = models.FloatField(default=0, verbose_name="[PROFID_DEDT_QOQ] 扣非后净利润环比增长")

    # net_profit_fy0 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY0] FY0总净利润")
    # net_profit_fy1 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY1] FY1总净利润")
    # net_profit_fy2 = models.FloatField(default=0, verbose_name="[NET_PROFIT_FY2] FY2总净利润")

    class Meta:
        index_together = [
            'quarter', 'stock_concept'
        ]


class ConceptStockDaily(models.Model):
    '''
    模型: 概念股日级统计
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    concepts = models.CharField(max_length=2048, verbose_name="[CONCEPTS] 所属概念")

    trade_date = models.DateField(verbose_name="[TRADE_DATE] 交易日期")

    change_rate = models.FloatField(default=0, verbose_name="[CHANGE_RATE] 涨跌幅")
    turnover_rate = models.FloatField(default=0, verbose_name="[TURNOVER_RATE] 换手率")
    float_share = models.FloatField(default=0, verbose_name="[FLOAT_SHARE] 流通股本")

    net_mf_d5 = models.FloatField(default=0, verbose_name="[NET_MF_D5] 5日净流入额")
    net_mf_d20 = models.FloatField(default=0, verbose_name="[NET_MF_D20] 20日净流入额")
    net_mf_d60 = models.FloatField(default=0, verbose_name="[NET_MF_D60] 60日净流入额")
    pos_mf_d5_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D5_CNT] 5日正净流入额天数")
    pos_mf_d20_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D20_CNT] 20日正净流入额天数")
    pos_mf_d60_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D60_CNT] 60日正净流入额天数")
    

class ConceptDaily(models.Model):
    '''
    模型: 概念日级统计
    '''
    ccode = models.CharField(max_length=16, db_index=True, verbose_name="[CCODE] 概念代码")
    cname = models.CharField(max_length=32, db_index=True, verbose_name="[CNAME] 概念名称")

    stock_basic_count = models.IntegerField(default=0, verbose_name="[STOCK_BASIC_COUNT] 成分股数量")

    trade_date = models.DateField(verbose_name="[TRADE_DATE] 交易日期")

    change_rate = models.FloatField(default=0, verbose_name="[CHANGE_RATE] 涨跌幅")
    turnover_rate = models.FloatField(default=0, verbose_name="[TURNOVER_RATE] 换手率")
    net_mf_d5 = models.FloatField(default=0, verbose_name="[NET_MF_D5] 5日净流入额")
    net_mf_d20 = models.FloatField(default=0, verbose_name="[NET_MF_D20] 20日净流入额")
    net_mf_d60 = models.FloatField(default=0, verbose_name="[NET_MF_D60] 60日净流入额")
    pos_mf_d5_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D5_CNT] 5日正净流入额天数")
    pos_mf_d20_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D20_CNT] 20日正净流入额天数")
    pos_mf_d60_cnt = models.IntegerField(default=0, verbose_name="[POS_MF_D60_CNT] 60日正净流入额天数")


class StockFreshPrice(models.Model):
    '''
    模型: 股价新高
    '''
    scode = models.CharField(max_length=16, db_index=True, editable=False, verbose_name="[SCODE] 股票代码")
    sname = models.CharField(max_length=16, db_index=True, verbose_name="[SNAME] 股票名称")
    industry = models.CharField(max_length=16, db_index=True, verbose_name="[INDUSTRY] 所属行业")
    concepts = models.CharField(max_length=512, verbose_name="[CONCEPTS] 所属概念")
    stock_basic = models.ForeignKey(StockBasic, editable=False, on_delete=models.CASCADE, verbose_name="[STOCK_BASIC] 股票基础信息")
    
    updated_date = models.DateField(auto_now_add=True, verbose_name="[UPDATE_DATE] 更新日期")

    close = models.FloatField(default=0, verbose_name="[CLOSE] 收盘价")
    change = models.FloatField(default=0, verbose_name="[CHANGE] 涨跌幅")

    duration_days = models.IntegerField(default=1, verbose_name="[DURATION_DAYS] 持续天数")
    extreme = models.BooleanField(default=False, verbose_name="[EXTREME] 历史最高")
